const os = require('os')
const fs = require('fs')
const path = require('path')

const _ = require('lodash')
const { XMLParser, XMLBuilder } = require('fast-xml-parser')
const JSZip = require('jszip')
const { nanoid } = require('nanoid')

function normalizePath(...paths) {
  return path.normalize(path.join(...paths))
}

function directory2Object(dir, parentKey = '') {
  const object = {}

  for (const file of fs.readdirSync(dir, { withFileTypes: true })) {
    const key = parentKey
      ? `${parentKey}/${file.name}`
      : file.name
    const filePath = normalizePath(dir, file.name)

    if (fs.lstatSync(filePath).isDirectory()) {
      Object.assign(object, directory2Object(filePath, key))
    } else {
      object[key] = fs.readFileSync(filePath)
    }
  }

  return object
}

export const PARSER_CONFIG = {
  parse: {
    attributeNamePrefix: '@_',
    ignorePiTags: true, // IMPORTANT
    textNodeName: '#text',
    ignoreAttributes: false,
    allowBooleanAttributes: true,
    parseAttributeValue: true,
    parseTagValue: false,
    trimValues: false,
    cdataPropName: '__cdata',
    numberParseOptions:{
      hex: true,
      leadingZeros: true
    },
    preserveOrder: true
  },
  build: {
    attributeNamePrefix: '@_',
    textNodeName: '#text',
    ignoreAttributes: false,
    cdataPropName: '__cdata',
    suppressEmptyNode: false,
    preserveOrder: true
  }
}

export async function getDocxEditor(buffer) {
  const docxName = `docx_${nanoid(8)}_${Date.now()}`
  const docxPath = normalizePath(os.tmpdir(), docxName)

  function mkdir(...relativePaths) {
    try {
      const toPath = relativePaths.length
        ? normalizePath(docxPath, ...relativePaths)
        : docxPath

        if (!fs.existsSync(toPath)) fs.mkdirSync(toPath)
    } catch (err) {}
  }

  // save zip-like docx on tmpdir
  try {
    mkdir()

    const zip = await JSZip.loadAsync(buffer, { createFolders: true })

    for (const key of Object.keys(zip.files)) {
      if (zip.files[key].dir) {
        mkdir(key)
        continue
      }
      await new Promise(resolve => {
        zip
          .file(key)
          .nodeStream()
          .pipe(
            fs.createWriteStream(
              normalizePath(docxPath, key)
            )
          )
          .on('finish', resolve)
      })
    }
  } catch (err) {
    console.error(err)
  }

  function parse(target, onParsed) {
    const targetPath = normalizePath(docxPath, target)
    const file = fs.readFileSync(targetPath, 'utf-8')

    try {
      const parser = new XMLParser(PARSER_CONFIG.parse)
      const xml = parser.parse(file)
      const cloneXml = _.cloneDeep(xml)

      if (typeof onParsed === 'function') {
        onParsed(xml)
      }

      if (!_.isEqual(cloneXml, xml)) {
        const builder = new XMLBuilder(PARSER_CONFIG.build)
        const updatedFile = builder.build(xml)

        fs.writeFileSync(targetPath, updatedFile)
      }
    } catch (err) {
      console.error(err)
    }
  }

  async function save() {
    const zip = new JSZip()
    const updatedZip = zip.folder(docxName)
    const docxObject = directory2Object(docxPath)

    for (const key in docxObject) {
      updatedZip.file(key, docxObject[key])
    }

    return await updatedZip.generateAsync({ type: 'nodebuffer' })
  }

  function stream() {
    const zip = new JSZip()
    const updatedZip = zip.folder(docxName)
    const docxObject = directory2Object(docxPath)

    for (const key in docxObject) {
      updatedZip.file(key, docxObject[key])
    }

    return updatedZip.generateNodeStream({ streamFiles: true })
  }

  function drop() {
    return fs.rmSync(docxPath, { recursive: true })
  }

  function writeFile(relativePath, data) {
    try {
      fs.writeFileSync(normalizePath(docxPath, relativePath), data)
    } catch (err) {}
  }

  return { mkdir, writeFile, parse, save, stream, drop }
}

export function createImageXML(rId, w, h, defaultImageHeightPielx = 32) {
  const size = {
    '@_cx': Math.ceil(defaultImageHeightPielx * 9525 * w / h),
    '@_cy': defaultImageHeightPielx * 9525
  }
  const desc = {
    '@_id': rId,
    '@_name': `替换图片 ${rId}`
  }
  return {
    'w:r': [
      { 'w:rPr': [{ 'w:u': [], ':@': { '@_w:val': 'single' } }] },
      {
        'w:drawing': [
          {
            'wp:inline': [
              { 'wp:extent': [], ':@': size },
              { 'wp:docPr': [], ':@': desc },
              { 'a:graphic': [{
                  'a:graphicData': [{
                    'pic:pic': [
                      { 'pic:nvPicPr': [{ 'pic:cNvPr': [], ':@': desc }] },
                      { 'pic:blipFill': [
                        { 'a:blip': [], ':@': { '@_r:embed': `rId${rId}` } },
                        { 'a:stretch': [{ 'a:fillRect': [] }]}
                      ] },
                      { 'pic:spPr': [
                        { 'a:xfrm': [
                          { 'a:off': [], ':@': { '@_x': 0, '@_y': 0 } },
                          { 'a:ext': [], ':@': size }
                        ] },
                        { 'a:prstGeom': [{ 'a:avLst': [] }], ':@': { '@_prst': 'rect' } }
                      ] }
                    ],
                    ':@': { '@_xmlns:pic': 'http://schemas.openxmlformats.org/drawingml/2006/picture' }
                  }],
                  ':@': { '@_uri': 'http://schemas.openxmlformats.org/drawingml/2006/picture' }
                }],
                ':@': { '@_xmlns:a': 'http://schemas.openxmlformats.org/drawingml/2006/main' }
              }
            ]
          }
        ]
      }
    ]
  }
}

export function createImageRelationXML(rId, relativePathInDocx) {
  return {
    Relationship: [],
    ':@': {
      '@_Id': `rId${rId}`,
      '@_Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
      '@_Target': relativePathInDocx
    }
  }
}

export async function getBookmarks(buffer) {
  const editor = await getDocxEditor(buffer)
  const bookmarks = []

  editor.parse('word/document.xml', xml => {
    const body = xml[0]['w:document'][0]['w:body']

    if (Array.isArray(body)) {
      body.forEach(item => {
        if (Array.isArray(item['w:p'])) {
          item['w:p'].forEach(subItem => {
            if (Array.isArray(subItem['w:bookmarkStart'])) {
              if (
                subItem[':@'] &&
                subItem[':@']['@_w:name'] !== '_GoBack'
              ) {
                bookmarks.push(subItem[':@']['@_w:name'])
              }
            }
          })
        }
      })
    }
  })
  editor.drop()

  return bookmarks
}
